package org.test4j.mock.faking.util;

import org.test4j.mock.Invocation;
import org.test4j.mock.functions.ESupplier;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.test4j.mock.faking.util.AsmConstant.*;
import static org.test4j.mock.faking.util.ClassFile.notObjectOrProxy;

public class ReflectUtility {
    public static final Pattern JAVA_LANG = Pattern.compile("java.lang.", Pattern.LITERAL);

    private static Throwable exceptionToThrow;

    /**
     * 通过 new Instance()的反射调用, 绕过对受检异常的封装
     *
     * @throws Exception
     */
    public ReflectUtility() throws Throwable {
        if (exceptionToThrow != null) {
            throw exceptionToThrow;
        }
    }

    /**
     * 抛出 checkedException 异常
     * 通过 new Instance()的反射调用, 绕过对受检异常的封装
     *
     * @param e
     * @return
     */
    public static synchronized <T> T doThrow(Throwable e) {
        if (e instanceof RuntimeException) {
            throw (RuntimeException) e;
        } else if (e instanceof Error) {
            throw (Error) e;
        }
        exceptionToThrow = e;
        try {
            ReflectUtility.class.newInstance();
        } catch (InstantiationException | IllegalAccessException ignore) {
        }
        return null;
    }

    /**
     * 根据签名描述查找构造函数或方法
     *
     * @param theClass
     * @param methodName
     * @param memberDesc
     * @return
     * @throws NoSuchMethodException
     */
    public static Executable findMethodByDesc(Class theClass, String methodName, String memberDesc) {
        Class[] paramTypes = TypeUtility.getParameterTypes(memberDesc);
        Class superClass = theClass;

        if (TypeUtility.isConstructor(methodName)) {
            for (Constructor declaredConstructor : superClass.getDeclaredConstructors()) {
                Class[] declaredParameterTypes = declaredConstructor.getParameterTypes();
                int firstRealParameter = indexOfFirstRealParameter(declaredParameterTypes, paramTypes);
                if (firstRealParameter == -1) {
                    continue;
                }
                if (acceptsArgumentTypes(declaredParameterTypes, paramTypes, firstRealParameter)) {
                    return declaredConstructor;
                }
            }
            throw new IllegalArgumentException("Specified constructor not found: " + toDescriptionMethod(theClass.getSimpleName(), paramTypes));
        } else {
            while (notObjectOrProxy(superClass)) {
                Method method = findMethodInClassOrInterface(superClass, methodName, paramTypes);
                if (method == null) {
                    superClass = superClass.getSuperclass();
                } else {
                    return method;
                }
            }
            throw new RuntimeException("NoSuchMethod:" + theClass.getName() + "#" + methodName + memberDesc);
        }
    }

    private static Method findMethodInClassOrInterface(Class theClass, String methodName, Class[] paramTypes) {
        try {
            Method method = theClass.getDeclaredMethod(methodName, paramTypes);
            return method.isBridge() ? null : method;
        } catch (NoSuchMethodException e) {
            for (Class anInterface : theClass.getInterfaces()) {
                try {
                    return anInterface.getMethod(methodName, paramTypes);
                } catch (NoSuchMethodException ignore) {
                }
            }
        }
        return null;
    }

    /**
     * 方法+参数描述
     *
     * @param method     方法名称
     * @param paramTypes 方法参数
     * @return
     */
    public static String toDescriptionMethod(String method, Class[] paramTypes) {
        return method + Stream.of(paramTypes)
            .map(Class::getCanonicalName)
            .map(name -> JAVA_LANG.matcher(name).replaceAll(""))
            .collect(Collectors.joining(", ", "(", ")"));
    }

    /**
     * 1: 第一个入参是Invocation, 排除掉后参数个数相等
     * 0: 参数个数相等
     * -1: 参数个数不等
     *
     * @param mockParameterTypes
     * @param realParameterTypes
     * @return
     */
    private static int indexOfFirstRealParameter(Class[] mockParameterTypes, Class[] realParameterTypes) {
        int extraParameters = mockParameterTypes.length - realParameterTypes.length;

        if (extraParameters == 1) {
            return mockParameterTypes[0] == Invocation.class ? 1 : -1;
        } else {
            return extraParameters != 0 ? -1 : 0;
        }
    }

    /**
     * 判断参数列表是否匹配
     *
     * @param declaredTypes  @Mock  方法声明的参数列表
     * @param specifiedTypes 真实调用的参数列表
     * @param firstParameter @Mock方法第一个参数是Invocation: 1, else: 0;
     * @return
     */
    private static boolean acceptsArgumentTypes(Class[] declaredTypes, Class[] specifiedTypes, int firstParameter) {
        for (int index = firstParameter; index < declaredTypes.length; index++) {
            Class declaredType = declaredTypes[index];
            Class specifiedType = specifiedTypes[index - firstParameter];
            return TypeUtility.isAssignable(declaredType, specifiedType);
        }
        return true;
    }

    /**
     * @param aClass
     * @param argument
     * @param <T>
     * @return
     */
    public static <T> T newInstance(Class aClass, String argument) {
        try {
            if (argument == null) {
                Constructor<T> constructor = aClass.getDeclaredConstructor();
                return invoke(constructor, () -> constructor.newInstance());
            } else {
                Constructor constructor = aClass.getDeclaredConstructor(String.class);
                return invoke(constructor, () -> constructor.newInstance(argument));
            }
        } catch (NoSuchMethodException e) {
            return doThrow(e);
        }
    }

    public static <T> T invoke(Object targetInstance, Method method, Object... methodArgs) {
        return invoke(method, () -> method.invoke(targetInstance, methodArgs));
    }

    public static <T> T invoke(Executable method, ESupplier supplier) {
        boolean accessible = method.isAccessible();
        try {
            method.setAccessible(true);
            Object obj = supplier.get();
            return (T) obj;
        } catch (IllegalArgumentException e) {
            StackTrace.filterStackTrace(e);
            throw new IllegalArgumentException("Failure to invoke method: " + method.toString(), e);
        } catch (InvocationTargetException e) {
            return doThrow(e.getCause());
        } catch (Throwable e) {
            return doThrow(e);
        } finally {
            method.setAccessible(accessible);
        }
    }

    /**
     * 返回对应的mock method name
     *
     * @param name
     * @return
     */
    public static String getCorrespondingFakeName(String name) {
        if (Method_Init.equals(name)) {
            return Method_$Init;
        }
        if (Method_CL_INIT.equals(name)) {
            return Method_$CL_Init;
        }
        return name;
    }
}